﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Net.Sockets;
using System.Runtime.CompilerServices;
using System.Linq;
using System.Diagnostics.CodeAnalysis;

namespace Bonobo.Irc
{
    public abstract class IrcConnection : IDisposable
    {
        private sealed class AsyncState
        {
            public readonly TcpClient Client;
            public readonly Stream Stream;
            public readonly Byte[] Buffer;

            public AsyncState(TcpClient client, Stream stream, Byte[] buffer)
            {
                Client = client;
                Stream = stream;
                Buffer = buffer;
            }
        }

        private static readonly Object EventMessageReceived = new Object();
        private static readonly Object EventStateChanged = new Object();

        private readonly IrcConnectionReceiveBuffer _receiveBuffer = new IrcConnectionReceiveBuffer(32768);
        private readonly EventHandlerList _events = new EventHandlerList();

        private volatile IrcConnectionState _state;
        private volatile Exception _faultReason;

        private TcpClient _client;
        private Stream _stream;
        private Uri _uri;

        protected IrcConnection(Uri uri)
        {
            if (uri == null)
            {
                throw new ArgumentNullException("uri");
            }

            if (uri.Scheme != IrcStyleUriParser.SchemeName)
            {
                throw new ArgumentException(Resources.IrcConnection_UriNotIrc);
            }

            _uri = uri;
        }

        protected IrcConnection(TcpClient client)
        {
            if (client == null)
            {
                throw new ArgumentNullException("client");
            }

            _client = client;
        }

        public event EventHandler<IrcMessageEventArgs> MessageReceived
        {
            add { _events.AddHandler(EventMessageReceived, value); }
            remove { _events.RemoveHandler(EventMessageReceived, value); }
        }

        public event EventHandler<IrcConnectionStateEventArgs> StateChanged
        {
            add { _events.AddHandler(EventStateChanged, value); }
            remove { _events.RemoveHandler(EventStateChanged, value); }
        }

        public Exception FaultReason
        {
            get { return _faultReason; }
        }

        public IrcConnectionState State
        {
            get { return _state; }
            private set
            {
                if (_state != value)
                {
                    _state = value;
                    OnStateChanged(new IrcConnectionStateEventArgs(value));
                }
            }
        }

        protected EventHandlerList Events
        {
            get { return _events; }
        }

        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        public void CloseAsync()
        {
            if (State != IrcConnectionState.Faulted)
            {
                State = IrcConnectionState.Closing;
                Dispose();
            }
        }

        public void OpenAsync()
        {
            EnsureCreated();
            BeginConnect(null);
        }

        public void SendAsync(params IrcMessage[] messages)
        {
            if (messages == null)
            {
                throw new ArgumentNullException("messages");
            }

            if (messages.Length > 0)
            {
                using (var ms = new MemoryStream())
                {
                    foreach (var m in messages)
                    {
                        m.WriteTo(ms);
                    }

                    BeginWrite(ms.ToArray());
                }
            }
        }

        protected virtual void Dispose(Boolean disposing)
        {
            if (disposing)
            {
                if (_events != null)
                {
                    _events.Dispose();
                }

                if (_stream != null)
                {
                    _stream.Close();
                    _stream = null;
                }

                if (_client != null)
                {
                    _client.Close();
                    _client = null;
                }

                if (State != IrcConnectionState.Faulted)
                {
                    State = IrcConnectionState.Closed;
                }
            }
        }

        protected virtual void OnMessageReceived(IrcMessageEventArgs e)
        {
            _events.InvokeEvent(EventMessageReceived, this, e);
        }

        protected virtual void OnStateChanged(IrcConnectionStateEventArgs e)
        {
            _events.InvokeEvent(EventStateChanged, this, e);
        }

        protected void Fault(Exception reason)
        {
            if (State == IrcConnectionState.Faulted)
            {
                throw new InvalidOperationException();
            }

            _state = IrcConnectionState.Faulted;
            _faultReason = reason;

            OnStateChanged(new IrcConnectionStateEventArgs(reason));
        }

        private void BeginConnect(Byte[] bufferToSend)
        {
            EnsureCreated();
            State = IrcConnectionState.Opening;

            if ((_client != null) && (_client.Connected))
            {
                _stream = _client.GetStream();
                _state = IrcConnectionState.Opened;

                BeginRead();
            }
            else
            {
                _client = new TcpClient();
                _client.BeginConnect(_uri.Host, _uri.Port, OnConnectCompleted, CreateAsyncState(bufferToSend));
            }
        }

        private void BeginRead()
        {
            if (State == IrcConnectionState.Created)
            {
                BeginConnect(null);
            }
            else if (State == IrcConnectionState.Opened)
            {
                _receiveBuffer.BeginReadFrom(_stream, OnReadCompleted, CreateAsyncState(null));
            }
        }

        private void BeginWrite(Byte[] buffer)
        {
            if (State == IrcConnectionState.Created)
            {
                BeginConnect(buffer);
            }
            else if (State == IrcConnectionState.Opened)
            {
                _stream.BeginWrite(buffer, 0, buffer.Length, OnWriteCompleted, CreateAsyncState(null));
            }
            else
            {
                throw new InvalidOperationException();
            }
        }

        private void OnConnectCompleted(IAsyncResult ar)
        {
            var asyncState = (AsyncState)(ar.AsyncState);

            try
            {
                asyncState.Client.EndConnect(ar);

                _client = asyncState.Client;
                _stream = asyncState.Client.GetStream();

                State = IrcConnectionState.Opened;
                BeginRead();

                if (asyncState.Buffer != null)
                {
                    BeginWrite(asyncState.Buffer);
                }
            }
            catch (Exception exc)
            {
                Fault(exc);
            }
        }

        private void OnReadCompleted(IAsyncResult ar)
        {
            var asyncState = (AsyncState)(ar.AsyncState);

            try
            {
                var read = _receiveBuffer.EndReadFrom(asyncState.Stream, ar);

                if ((State == IrcConnectionState.Opened) && (read > 0))
                {
                    foreach (var msg in _receiveBuffer.ReadMessages())
                    {
                        OnMessageReceived(new IrcMessageEventArgs(msg));
                    }

                    BeginRead();
                }
            }
            catch (Exception exc)
            {
                Fault(exc);
            }
        }

        private void OnWriteCompleted(IAsyncResult ar)
        {
            var asyncState = (AsyncState)(ar.AsyncState);

            try
            {
                asyncState.Stream.EndWrite(ar);
            }
            catch (Exception exc)
            {
                Fault(exc);
            }
        }

        private AsyncState CreateAsyncState(Byte[] buffer)
        {
            return new AsyncState(_client, _stream, buffer);
        }

        private void EnsureCreated()
        {
            EnsureNotClosed();

            if (State != IrcConnectionState.Created)
            {
                throw new InvalidOperationException();
            }
        }

        private void EnsureNotClosed()
        {
            if (State == IrcConnectionState.Closed)
            {
                throw new ObjectDisposedException(GetType().Name);
            }
        }
    }
}